[FaaS] Create multiple function with fn #fnproject
こむろ@札幌です。まだ札幌は雪が降っていません。異常気象だ。
はじめに
fnを使い単体のFunctionについて確認しました。大量のFunctionを構成する場合、すべてがフラットに構成されてしまうと非常に見づらく(管理しづらく)なってしまいます。そこで、今回はグルーピングして複数のFunctionが管理できるかを検証してみます。
こんな感じになっているのを。
理想はこうできると良いな、という検証です。
fnは発展途上のOSSです。今回紹介する内容は未来(次の日かも知れない)で正しくなくなる可能性があります。その点を踏まえた上で必要ならばご参照ください。
前提
今回のバージョンは以下(2018/11/12現在)になります。
- fn version 0.5.26
- fn-server v0.3.600
Grouping function
Functionを入れ子にすることでGroupingさせることができます。今回はこんな構成を目指します。
nest-function ├── func.yaml ├── goodbye │ ├── func.java │ └── func.yaml └── hello ├── func.ruby └── func.yaml
Rootであるnest-function, 子要素のFunctionに2つのFunction、hello, goodbye を作成します。
Create Nested Function
では作っていきます。せっかくなのでいくつかのruntimeを混ぜたFunctionを構成してみましょう。
Create Parent Directory
nest-function
という名前のディレクトリを作成し、 app.yaml
を作成しておきます。Deployするアプリケーション名が定義されているファイルです。
name: nest-sample-function
これを定義しておくとdeploy時にアプリケーション名を指定せずとも良いようです。この時点での構造は以下の通り。
nest-function/ └── app.yaml
Create Child Functions
続いて子Functionを作成します。先程作成したFunctionの nest-function
ディレクトリへ移動します。
Java Function
hello
FunctionをJavaで作成します。
$ fn init --runtime java --trigger http hello Creating function at: /hello Function boilerplate generated. func.yaml created.
こちらも一部コードを修正します。
package com.example.fn; public class HelloFunction { public String handleRequest(String input) { String name = (input == null || input.isEmpty()) ? "world" : input; return "Hello, Java " + name + "!"; } }
こちらも大した修正は入れていません。Java
を追記したのみ。Javaはテストコードも記述されています。こちらも修正します。
public class HelloFunctionTest { @Rule public final FnTestingRule testing = FnTestingRule.createDefault(); @Test public void shouldReturnGreeting() { testing.givenEvent().enqueue(); testing.thenRun(HelloFunction.class, "handleRequest"); FnResult result = testing.getOnlyResult(); assertEquals("Hello, Java world!", result.getBodyAsString());} }
Python Function
最後に goodbye
FunctionはPythonで作成します。
$ fn init --runtime python --trigger http goodbye Creating function at: /goodbye Function boilerplate generated. func.yaml created.
こちらも一部コードを修正します。
def handler(ctx, data=None, loop=None): name = "World" if data and len(data) > 0: body = json.loads(data) name = body.get("name") return {"message": "Hello Python {0}".format(name)}
準備ができました。
Deploy to Local
現在の構成を確認してみます。
$ tree nest-function/ nest-function/ ├── app.yaml ├── goodbye │ ├── func.py │ ├── func.yaml │ └── requirements.txt └── hello ├── func.yaml ├── pom.xml └── src ├── main │ └── java │ └── com │ └── example │ └── fn │ └── HelloFunction.java └── test └── java └── com └── example └── fn └── HelloFunctionTest.java
Javaのパッケージ構造がある関係上ディレクトリの深度が増している感じですが、`nest-function以下にPython, JavaのFunctionコードが存在することが分かります。これをDeployしていきます。
Deploy to Local Environment
まずはRegistryへImageをPushせずにローカル環境へのDeployを実行します。 nest-function
ディレクトリ以下でコマンドを実行します。
$ fn deploy --all --local Deploying goodbye to app: nest-sample-function Bumped to version 0.0.4 Building image hogeuser/goodbye:0.0.4 ..................... Updating function goodbye using image hogeuser/goodbye:0.0.4... Successfully created function: goodbye with hogeuser/goodbye:0.0.4 Successfully created trigger: goodbye Trigger Endpoint: http://localhost:8999/t/nest-sample-function/goodbye-trigger Deploying hello to app: nest-sample-function Bumped to version 0.0.3 Building image hogeuser/hello:0.0.3 Updating function hello using image hogeuser/hello:0.0.3... Successfully created function: hello with hogeuser/hello:0.0.3 Successfully created trigger: hello Trigger Endpoint: http://localhost:8999/t/nest-sample-function/hello-trigger
デプロイが完了しました。ディレクトリ配下に作成した2つのFunctionがDeployされていることがわかります。
Check Triggers
TriggerがFunctionの数だけ生成されています。念の為、定義されているTriggerを確認してみます。
$ fn list triggers nest-sample-function FUNCTION NAME ID TYPE SOURCE ENDPOINT goodbye goodbye 01CWB8CPD5NG8G00GZJ0000003 http /goodbye-trigger http://localhost:8999/t/nest-sample-function/goodbye-trigger hello hello 01CWB8CQ8TNG8G00GZJ0000005 http /hello-trigger http://localhost:8999/t/nest-sample-function/hello-trigger
意図通り2つのFunction Triggerが定義されていました。
Invoke Functions
それぞれのFunctionを実行してみましょう。
$ fn invoke nest-sample-function hello Hello, Java world! $ fn invoke nest-sample-function goodbye {"message":"Hello Python World"}
異なるruntimeで構成されたFunctionをそれぞれちゃんと実行できました。ディレクトリの中にネストされたFunctionであっても --all
の指定ですべてDeployし、実行することができることが確認できました。
Nested Function?
ところで、確かにFunctionのコードは入れ子で構成できましたが、実際に実行するFunctionのTriggerはフラットな構造になっており、本来の意味での入れ子構造になっていません。
元の想定とはちょっと異なります。理想としてFunction Triggerの構造はディレクトリの構造を反映して以下のようになってほしいところです。
http://localhost:8999/t/nest-sample-function/nest-function/goodbye-trigger ## Child Function 1 http://localhost:8999/t/nest-sample-function/nest-function/hello-trigger ## Child Function 2
nest-sample-function
はアプリケーション名なので固定です。この形を目指してまずはTriggerの構造を変更していきます。
FunctionのTrigger定義を修正
Deploy時にTrigger定義を生成する際には func.yaml
の定義を利用します。そこでこのTrigger定義をそれぞれ書き換える必要があります。
まずは hello
Functionの func.yaml
の中にある triggers/source
を書き換えます。
schema_version: 20180708 name: hello version: 0.0.3 runtime: java build_image: fnproject/fn-java-fdk-build:jdk9-1.0.75 run_image: fnproject/fn-java-fdk:jdk9-1.0.75 cmd: com.example.fn.HelloFunction::handleRequest format: http-stream triggers: - name: hello type: http source: /nest-function/hello-trigger
goodbye
Functionの triggers/source
も書き換えます。
schema_version: 20180708 name: goodbye version: 0.0.4 runtime: python entrypoint: python3 func.py format: http-stream triggers: - name: goodbye type: http source: /nest-function/goodbye-trigger
定義の変更が完了したので、再度Deployを行えばTriggerの定義更新を行ってくれるはずです。
再度デプロイを実行
定義を変更した内容をDeployしてみます。
$ fn deploy --all --local Deploying hello to app: nest-sample-function Bumped to version 0.0.4 Building image hogeuser/hello:0.0.4 ...... Updating function nest-function using image hogeuser/hello:0.0.4... Fn: deploy error on /Users/xxxxxxxx/fn/nest-function/hello/func.yaml: Trigger with the same type and source exists on this app See 'fn <command> --help' for more information. Client version: 0.5.26
おや。エラーで失敗しました。どうやら現時点ではTriggerの定義はDeployタスクによって更新できないようです。これは厳しい。
Trigger定義を削除する
現在はTriggerを更新する方法がなさそうなので、仕方なく手動ですべてのTriggerを削除します。
$ fn delete trigger nest-sample-function hello hello-trigger nest-sample-function hello hello-trigger deleted $ fn delete trigger nest-sample-function goodbye goodbye-trigger nest-sample-function goodbye goodbye-trigger deleted
delete trigger
コマンドではfn-serverに登録されているTriggerの定義を削除できます。引数はそれぞれ アプリケーション名, Function名, Trigger名 の順番で指定します。いずれもrequiredな引数なので省略はできません。
上記コマンド後にTriggerが削除されたかを確認してみます。
$ fn list triggers nest-sample-function schema_version: 20180708 No triggers found for app: nest-sample-function
すべて削除されました。
再再度デプロイを実行
もう一度Deployを実行してみましょう *1
$ fn deploy --all --local Deploying goodbye to app: nest-sample-function Bumped to version 0.0.4 Building image hogeuser/goodbye:0.0.4 ..................... Updating function goodbye using image hogeuser/goodbye:0.0.4... Successfully created function: goodbye with hogeuser/goodbye:0.0.4 Successfully created trigger: goodbye Trigger Endpoint: http://localhost:8999/t/nest-sample-function/nest-function/goodbye-trigger Deploying hello to app: nest-sample-function Bumped to version 0.0.3 Building image hogeuser/hello:0.0.3 Updating function hello using image hogeuser/hello:0.0.3... Successfully created function: hello with hogeuser/hello:0.0.3 Successfully created trigger: hello Trigger Endpoint: http://localhost:8999/t/nest-sample-function/nest-function/hello-trigger
今度は正常にDeployが完了するはずです。Triggerの定義を確認します。
$ fn list triggers nest-sample-function FUNCTION NAME ID TYPE SOURCE ENDPOINT goodbye goodbye 01CWB8CPD5NG8G00GZJ0000003 http /nest-function/goodbye-trigger http://localhost:8999/t/nest-sample-function/nest-function/goodbye-trigger hello hello 01CWB8CQ8TNG8G00GZJ0000005 http /nest-function/hello-trigger http://localhost:8999/t/nest-sample-function/nest-function/hello-trigger
想定通りのTriggerが作成できました。実行してみます。まずは hello
Functionから
$ curl -H "Content-Type: text/plain" -d 'Classmethod' http://localhost:8999/t/nest-sample-function/nest-function/hello-trigger Hello, Java Classmethod!
続いて goodbye
Functionを実行。
$ curl -H "Content-Type: application/json" -d '{"name": "Classmethod"}' http://localhost:8999/t/nest-sample-function/nest-function/goodbye-trigger {"message":"Hello Python Classmethod"}
これで見た目上ではありますが、グルーピングされたFunctionをパス上は表現できました。些か不満ではありますがひとまずの目的は達成しました。
Trouble Shooting
ここにたどり着くまでにいくつか失敗しているのでそちらを記載します。
その1. テストコードの修正忘れ
Javaのボイラープレートコードにはテストコードが記述されています。これの修正を忘れるとdeploy時にエラーが発生し処理が正常に完了しません。Functionのビルドに失敗した場合、以下のエラーが出力されます。
Building image hogeuser/hello:0.0.2 ......... Error during build. Run with `--verbose` flag to see what went wrong. eg: `fn --verbose CMD` Fn: deploy error on /Users/xxxxxxxx/fn/nest-function/hello/func.yaml: error running docker build: exit status 1
この出力からエラーの原因を確認するのはなかなか厳しそうです。
そこで原因を確認するためにhello/
ディレクトリ以下に入って fn build
を --verbose
オプション付きで実行し、単体でのBuildを試行してみました。 *2
$ fn --verbose build Building image hogeuser/hello:0.0.2 FN_REGISTRY: hogeuser Current Context: default Sending build context to Docker daemon 14.34kB Step 1/11 : FROM fnproject/fn-java-fdk-build:jdk9-1.0.75 as build-stage ---> 10c10a1cd2ae Step 2/11 : WORKDIR /function ---> Using cache ---> 14635a08065b Step 3/11 : ENV MAVEN_OPTS -Dhttp.proxyHost= -Dhttp.proxyPort= -Dhttps.proxyHost= -Dhttps.proxyPort= -Dhttp.nonProxyHosts= -Dmaven.repo.local=/usr/share/maven/ref/repository ---> Using cache ---> 8ecb8920ffaa Step 4/11 : ADD pom.xml /function/pom.xml ---> Using cache ---> eb08f93b98c9 Step 5/11 : RUN ["mvn", "package", "dependency:copy-dependencies", "-DincludeScope=runtime", "-DskipTests=true", "-Dmdep.prependGroupId=true", "-DoutputDirectory=target", "--fail-never"] ---> Using cache ---> 4ac15f343db0 Step 6/11 : ADD src /function/src ---> Using cache ---> 2b667837a3ab Step 7/11 : RUN ["mvn", "package"] ---> Running in fb38bc33b4e9 [INFO] Scanning for projects... [INFO] [INFO] ------------------------< com.example.fn:hello >------------------------ [INFO] Building hello 1.0.0 [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] skip non existing resourceDirectory /function/src/main/resources [INFO] [INFO] --- maven-compiler-plugin:3.3:compile (default-compile) @ hello --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 1 source file to /function/target/classes [INFO] [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ hello --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] skip non existing resourceDirectory /function/src/test/resources [INFO] [INFO] --- maven-compiler-plugin:3.3:testCompile (default-testCompile) @ hello --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 1 source file to /function/target/test-classes [INFO] [INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ hello --- [INFO] Surefire report directory: /function/target/surefire-reports ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.example.fn.HelloFunctionTest Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.632 sec <<< FAILURE! shouldReturnGreeting(com.example.fn.HelloFunctionTest) Time elapsed: 0.199 sec <<< FAILURE! org.junit.ComparisonFailure: expected:<Hello, []world!> but was:<Hello, [Java ]world!> at org.junit.Assert.assertEquals(Assert.java:115) at org.junit.Assert.assertEquals(Assert.java:144) at com.example.fn.HelloFunctionTest.shouldReturnGreeting(HelloFunctionTest.java:19) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:564) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) package com.example.fn; at org.junit.rules.RunRules.evaluate(RunRules.java:20) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:252) at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:141) at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:564) at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189) at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165) at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85) at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115) at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75) Results : Failed tests: shouldReturnGreeting(com.example.fn.HelloFunctionTest): expected:<Hello, []world!> but was:<Hello, [Java ]world!> Tests run: 1, Failures: 1, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.384 s [INFO] Finished at: 2018-11-12T09:00:07Z [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project hello: There are test failures. [ERROR] [ERROR] Please refer to /function/target/surefire-reports for the individual test results. [ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException The command 'mvn package' returned a non-zero code: 1 Fn: error running docker build: exit status 1
テスト失敗の詳細が表示されているのが分かります。具体的なエラーも出力されているため、この指摘どおりテストコードを修正してあげればこの問題は解消です。
その2. ネストしたFunctionでもDocker ImageはRegistryへフラットにPushされる
Functionは1つにつき1つのDocker Imageとして構成されます。ディレクトリの構造がネストしたFunctionの場合、パスを見れば別の関数と認識できるものの、実際のImage名は関数名のみで構成されています。そのため同じ関数名のImageがある場合、当然ながらPushすることができません。
以下具体例で示します。
$ tree nest-function2 nest-function2 ├── app.yaml ├── goodbye │ ├── Gopkg.toml │ ├── func.go │ └── func.yaml └── hello ├── Gopkg.toml ├── func.go └── func.yaml
nest-function2
以下に同じ名前のFunction hello
, goodby
を配置しました。いずれもTriggerは nest-function2
以下になるようにしています。
$ fn deploy --all Deploying goodbye to app: nest-sample-function2 Bumped to version 0.0.3 Building image hogeuser/goodbye:0.0.3 ........ Parts: [hogeuser goodbye:0.0.3] Pushing hogeuser/goodbye:0.0.3 to docker registry...The push refers to repository [docker.io/hogeuser/goodbye] 078fa38e1cee: Preparing d15d26d63fb7: Preparing 97dedccb7128: Preparing c9e8b5c053a2: Preparing denied: requested access to the resource is denied Fn: deploy error on /Users/xxxxxxxxx/fn/nest-function2/goodbye/func.yaml: error running docker push, are you logged into docker?: exit status 1 See 'fn <command> --help' for more information. Client version: 0.5.26
同じ名前でDocker ImageをPushしようとするのでダメ。Docker imageの名前が別途一意になるように修正が必要なようです。例えば nest-function2-goodbye
等。
まとめ
複数のFunctionをディレクトリ配下においてグルーピングし、Deployできることが分かりました。
もう少しきちんとグルーピングができるならば、Functionの管理が楽になりそうだと思ったのですが、少々手を加える必要があります。ネストしたFunctionを利用するにはもう少し工夫が必要なようです。
参照
- fn full documentation - Applications
- Medium - Deploy Full Serverless Applications with a Single Command